use env_logger::{Builder, Target};
use log::{error, info};
use std::collections::HashMap;
use std::fs;
use std::io;
use std::io::Read;
use std::time::Instant;

const KEY_COUNT: usize = 16;
const MEMORY_SIZE: usize = 4096;
const REGISTER_COUNT: usize = 16;
const STACK_LEVELS: usize = 16;
const VIDEO_HEIGHT: usize = 32;
const VIDEO_WIDTH: usize = 64;

const FONTSET: [u8; 80] = [
    0xF0, 0x90, 0x90, 0x90, 0xF0, // 0
    0x20, 0x60, 0x20, 0x20, 0x70, // 1
    0xF0, 0x10, 0xF0, 0x80, 0xF0, // 2
    0xF0, 0x10, 0xF0, 0x10, 0xF0, // 3
    0x90, 0x90, 0xF0, 0x10, 0x10, // 4
    0xF0, 0x80, 0xF0, 0x10, 0xF0, // 5
    0xF0, 0x80, 0xF0, 0x90, 0xF0, // 6
    0xF0, 0x10, 0x20, 0x40, 0x40, // 7
    0xF0, 0x90, 0xF0, 0x90, 0xF0, // 8
    0xF0, 0x90, 0xF0, 0x10, 0xF0, // 9
    0xF0, 0x90, 0xF0, 0x90, 0x90, // A
    0xE0, 0x90, 0xE0, 0x90, 0xE0, // B
    0xF0, 0x80, 0x80, 0x80, 0xF0, // C
    0xE0, 0x90, 0x90, 0x90, 0xE0, // D
    0xF0, 0x80, 0xF0, 0x80, 0xF0, // E
    0xF0, 0x80, 0xF0, 0x80, 0x80, // F
];

struct Chip8 {
    registers: [u8; REGISTER_COUNT],
    memory: [u8; MEMORY_SIZE],
    index: u16,
    pc: u16,
    stack: [u16; STACK_LEVELS],
    sp: u8,
    delay_timer: u8,
    sound_timer: u8,
    keypad: [bool; KEY_COUNT],
    video: [u32; VIDEO_WIDTH * VIDEO_HEIGHT],
    opcode: u16,
    op_table: HashMap<u16, fn(&mut Chip8)>,
    op_table0: HashMap<u16, fn(&mut Chip8)>,
    op_table8: HashMap<u16, fn(&mut Chip8)>,
    op_tablee: HashMap<u16, fn(&mut Chip8)>,
    op_tablef: HashMap<u16, fn(&mut Chip8)>,
}
impl Chip8 {
    fn new() -> Self {
        let mut memory = [0; MEMORY_SIZE];
        for i in 0..FONTSET.len() {
            memory[50 + i] = FONTSET[i];
        }
        let mut op_table: HashMap<u16, fn(&mut Chip8)> = HashMap::new();
        let mut op_table0: HashMap<u16, fn(&mut Chip8)> = HashMap::new();
        let mut op_table8: HashMap<u16, fn(&mut Chip8)> = HashMap::new();
        let mut op_tablee: HashMap<u16, fn(&mut Chip8)> = HashMap::new();
        let mut op_tablef: HashMap<u16, fn(&mut Chip8)> = HashMap::new();
        op_table.insert(0x0, Chip8::table0);
        op_table.insert(0x1, Chip8::op_1nnn);
        op_table.insert(0x2, Chip8::op_2nnn);
        op_table.insert(0x3, Chip8::op_3xkk);
        op_table.insert(0x4, Chip8::op_4xkk);
        op_table.insert(0x5, Chip8::op_5xy0);
        op_table.insert(0x6, Chip8::op_6xkk);
        op_table.insert(0x7, Chip8::op_7xkk);
        op_table.insert(0x8, Chip8::table8);
        op_table.insert(0x9, Chip8::op_9xy0);
        op_table.insert(0xa, Chip8::op_annn);
        op_table.insert(0xb, Chip8::op_bnnn);
        op_table.insert(0xc, Chip8::op_cxkk);
        op_table.insert(0xd, Chip8::op_dxyn);
        op_table.insert(0xe, Chip8::tablee);
        op_table.insert(0xf, Chip8::tablef);
        op_table0.insert(0x0, Chip8::op_00e0);
        op_table0.insert(0xe, Chip8::op_00ee);
        op_table8.insert(0x0, Chip8::op_8xy0);
        op_table8.insert(0x1, Chip8::op_8xy1);
        op_table8.insert(0x2, Chip8::op_8xy2);
        op_table8.insert(0x3, Chip8::op_8xy3);
        op_table8.insert(0x4, Chip8::op_8xy4);
        op_table8.insert(0x5, Chip8::op_8xy5);
        op_table8.insert(0x6, Chip8::op_8xy6);
        op_table8.insert(0x7, Chip8::op_8xy7);
        op_table8.insert(0xe, Chip8::op_8xye);
        op_tablee.insert(0x1, Chip8::op_exa1);
        op_tablee.insert(0xe, Chip8::op_ex9e);
        op_tablef.insert(0x07, Chip8::op_fx07);
        op_tablef.insert(0x0a, Chip8::op_fx0a);
        op_tablef.insert(0x15, Chip8::op_fx15);
        op_tablef.insert(0x18, Chip8::op_fx18);
        op_tablef.insert(0x1e, Chip8::op_fx1e);
        op_tablef.insert(0x29, Chip8::op_fx29);
        op_tablef.insert(0x33, Chip8::op_fx33);
        op_tablef.insert(0x55, Chip8::op_fx55);
        op_tablef.insert(0x65, Chip8::op_fx65);

        Chip8 {
            registers: [0; REGISTER_COUNT],
            memory,
            index: 0,
            pc: 0x200, // Program counter starts at 0x200
            stack: [0; STACK_LEVELS],
            sp: 0,
            delay_timer: 0,
            sound_timer: 0,
            keypad: [false; KEY_COUNT],
            video: [0; VIDEO_WIDTH * VIDEO_HEIGHT],
            opcode: 0,
            op_table,
            op_table0,
            op_table8,
            op_tablee,
            op_tablef,
        }
    }
    fn load_rom(&mut self, path: &str) -> Result<(), std::io::Error> {
        info!(target: "emulator", "Loading ROM from {}", path);

        match fs::File::open(path) {
            Ok(mut file) => {
                let mut buffer = [0; MEMORY_SIZE];
                match file.read(&mut buffer) {
                    Ok(bytes_read) => {
                        info!(target: "emulator", "Loaded {} bytes fro ROM", bytes_read);
                        for i in 0..bytes_read {
                            self.memory[self.pc as usize + i] = buffer[i];
                        }

                        Ok(())
                    }
                    Err(e) => {
                        error!("Error reading file: {}", e);
                        Err(io::Error::new(
                            io::ErrorKind::Other,
                            "Failed to read ROM from file",
                        ))
                    }
                }
            }
            Err(e) => {
                error!("Error opening file: {}", e);
                Err(io::Error::new(
                    io::ErrorKind::NotFound,
                    "Failed to open ROM file",
                ))
            }
        }
    }

    fn table0(&mut self) {
        if let Some(&func) = self.op_table0.get(&(self.opcode & 0x000f)) {
            func(self);
        }
    }
    fn table8(&mut self) {
        if let Some(&func) = self.op_table8.get(&(self.opcode & 0x000f)) {
            func(self);
        }
    }
    fn tablee(&mut self) {
        if let Some(&func) = self.op_tablee.get(&(self.opcode & 0x000f)) {
            func(self);
        }
    }
    fn tablef(&mut self) {
        if let Some(&func) = self.op_tablef.get(&(self.opcode & 0x00ff)) {
            func(self);
        }
    }
    fn op_00e0(&mut self) {
        for i in 0..self.video.len() {
            self.video[i] = 0;
        }
    }

    fn op_00ee(&mut self) {
        self.sp -= 1;
        self.pc = self.stack[self.sp as usize];
    }

    fn op_1nnn(&mut self) {
        self.pc = self.opcode & 0x0fff;
    }

    fn op_2nnn(&mut self) {
        self.stack[self.sp as usize] = self.pc;
        self.sp += 1;
        self.pc = self.opcode & 0x0fff;
    }

    fn op_3xkk(&mut self) {
        let vx = (self.opcode & 0x0f00) >> 8_u16;
        let byte = self.opcode as u8 & 0x00ff;
        if self.registers[vx as usize] == byte {
            self.pc += 2;
        }
    }

    fn op_4xkk(&mut self) {
        let vx = (self.opcode & 0x0f00) >> 8_u16;
        let byte = self.opcode as u8 & 0x00ff;
        if self.registers[vx as usize] != byte {
            self.pc += 2;
        }
    }

    fn op_5xy0(&mut self) {
        let vx = (self.opcode & 0x0f00) >> 8_u16;
        let vy = (self.opcode & 0x00f0) >> 4_u16;
        if self.registers[vx as usize] == self.registers[vy as usize] {
            self.pc += 2;
        }
    }

    fn op_6xkk(&mut self) {
        let vx = (self.opcode & 0x0f00) >> 8_u16;
        let byte = self.opcode & 0x00ff;
        self.registers[vx as usize] = byte as u8;
    }

    fn op_7xkk(&mut self) {
        let vx = (self.opcode & 0x0f00) >> 8_u16;
        let byte = self.opcode & 0x00ff;
        self.registers[vx as usize] += byte as u8;
    }

    fn op_8xy0(&mut self) {
        let vx = (self.opcode & 0x0f00) >> 8_u16;
        let vy = (self.opcode & 0x00f0) >> 4_u16;
        self.registers[vx as usize] = self.registers[vy as usize];
    }

    fn op_8xy1(&mut self) {
        let vx = (self.opcode & 0x0f00) >> 8_u16;
        let vy = (self.opcode & 0x00f0) >> 4_u16;
        self.registers[vx as usize] = self.registers[vx as usize] | self.registers[vy as usize];
    }
    fn op_8xy2(&mut self) {
        let vx = (self.opcode & 0x0f00) >> 8_u16;
        let vy = (self.opcode & 0x00f0) >> 4_u16;
        self.registers[vx as usize] = self.registers[vx as usize] & self.registers[vy as usize];
    }
    fn op_8xy3(&mut self) {
        let vx = (self.opcode & 0x0f00) >> 8_u16;
        let vy = (self.opcode & 0x00f0) >> 4_u16;
        self.registers[vx as usize] = self.registers[vx as usize] ^ self.registers[vy as usize];
    }

    fn op_8xy4(&mut self) {
        let vx = (self.opcode & 0x0f00) >> 8_u16;
        let vy = (self.opcode & 0x00f0) >> 4_u16;
        let sum = self.registers[vx as usize] as usize + self.registers[vy as usize] as usize;
        if sum > 255_usize {
            self.registers[0xf] = 1;
        } else {
            self.registers[0xf] = 0;
        }
        self.registers[vx as usize] = sum as u8 & 0xff;
    }
    fn op_8xy5(&mut self) {
        let vx = (self.opcode & 0x0f00) >> 8_u16;
        let vy = (self.opcode & 0x00f0) >> 4_u16;
        if self.registers[vx as usize] > self.registers[vy as usize] {
            self.registers[0xf] = 1;
        } else {
            self.registers[0xf] = 0;
        }
        self.registers[vx as usize] -= self.registers[vy as usize];
    }
    fn op_8xy6(&mut self) {
        let vx = (self.opcode & 0x0f00) >> 8_u16;
        self.registers[0xf] = self.registers[vx as usize] & 0x1;
        self.registers[vx as usize] >>= 1;
    }
    fn op_8xy7(&mut self) {
        let vx = (self.opcode & 0x0f00) >> 8_u16;
        let vy = (self.opcode & 0x00f0) >> 4_u16;
        if self.registers[vy as usize] > self.registers[vx as usize] {
            self.registers[0xf] = 1;
        } else {
            self.registers[0xf] = 0;
        }
        self.registers[vx as usize] = self.registers[vy as usize] - self.registers[vx as usize];
    }
    fn op_8xye(&mut self) {
        let vx = (self.opcode & 0x0f00) >> 8_u16;
        self.registers[0xf] = (self.registers[vx as usize] & 0x80) >> 7;
        self.registers[vx as usize] <<= 1;
    }
    fn op_9xy0(&mut self) {
        let vx = (self.opcode & 0x0f00) >> 8_u16;
        let vy = (self.opcode & 0x00f0) >> 4_u16;
        if self.registers[vx as usize] != self.registers[vy as usize] {
            self.pc += 2;
        }
    }

    fn op_annn(&mut self) {
        self.index = self.opcode & 0x0fff;
    }
    fn op_bnnn(&mut self) {
        self.pc = self.registers[0] as u16 + (self.opcode & 0x0fff);
    }
    fn op_cxkk(&mut self) {
        let vx = (self.opcode & 0x0f00) >> 8_u16;
        let byte = self.opcode & 0x00ff;

        self.registers[vx as usize] = (self.rand() & byte) as u8;
    }

    fn rand(&self) -> u16 {
        rand::random::<u16>()
    }

    fn op_dxyn(&mut self) {
        let vx = ((self.opcode & 0x0F00) >> 8) as usize;
        let vy = ((self.opcode & 0x00F0) >> 4) as usize;
        let height = (self.opcode & 0x000F) as usize;

        // Wrap if going beyond screen boundaries
        let x_pos = self.registers[vx] % VIDEO_WIDTH as u8;
        let y_pos = self.registers[vy] % VIDEO_HEIGHT as u8;

        self.registers[0xF] = 0;

        for row in 0..height {
            let sprite_byte = self.memory[self.index as usize + row];

            for col in 0..8 {
                let sprite_pixel = (sprite_byte & (0x80u8 >> col)) != 0;
                let screen_x = (x_pos as usize + col) % VIDEO_WIDTH;
                let screen_y = (y_pos as usize + row) % VIDEO_HEIGHT;
                let screen_index = screen_y * VIDEO_WIDTH + screen_x;
                let mut screen_pixel = self.video[screen_index];

                if sprite_pixel {
                    // Screen pixel also on - collision
                    if screen_pixel == 0xFFFFFFFF {
                        self.registers[0xF] = 1;
                    }

                    // Effectively XOR with the sprite pixel
                    screen_pixel ^= 0xFFFFFFFF;
                    self.video[screen_index] = screen_pixel;
                }
            }
        }
    }

    fn op_ex9e(&mut self) {
        let vx = (self.opcode & 0x0f00) >> 8_u16;
        let key = self.registers[vx as usize];
        if self.keypad[key as usize] {
            self.pc += 2;
        }
    }

    fn op_exa1(&mut self) {
        let vx = (self.opcode & 0x0f00) >> 8_u16;
        let key = self.registers[vx as usize];
        if !self.keypad[key as usize] {
            self.pc += 2;
        }
    }

    fn op_fx07(&mut self) {
        let vx = (self.opcode & 0x0f00) >> 8_u16;
        self.registers[vx as usize] = self.delay_timer;
    }

    fn op_fx0a(&mut self) {
        let vx = (self.opcode & 0x0f00) >> 8_u16;
        let mut f = true;
        for i in 0..KEY_COUNT {
            if self.keypad[i] {
                self.registers[vx as usize] = i as u8;
                f = false;
                break;
            }
        }
        if f {
            self.pc -= 2;
        }
    }

    fn op_fx15(&mut self) {
        let vx = (self.opcode & 0x0f00) >> 8_u16;
        self.delay_timer = self.registers[vx as usize];
    }

    fn op_fx18(&mut self) {
        let vx = (self.opcode & 0x0f00) >> 8_u16;
        self.sound_timer = self.registers[vx as usize];
    }

    fn op_fx1e(&mut self) {
        let vx = (self.opcode & 0x0f00) >> 8_u16;
        self.index += self.registers[vx as usize] as u16;
    }

    fn op_fx29(&mut self) {
        let vx = (self.opcode & 0x0f00) >> 8_u16;
        let digit = self.registers[vx as usize] as u16;
        self.index = 0x50 + (5 * digit);
    }

    fn op_fx33(&mut self) {
        let vx = (self.opcode & 0x0f00) >> 8_u16;
        let mut value = self.registers[vx as usize];
        self.memory[self.index as usize + 2] = value % 10;
        value /= 10;
        self.memory[self.index as usize + 2] = value % 10;
        value /= 10;
        self.memory[self.index as usize + 2] = value % 10;
    }

    fn op_fx55(&mut self) {
        let vx = ((self.opcode & 0x0f00) >> 8_u16) as usize;
        for i in 0..=vx {
            self.memory[self.index as usize + i] = self.registers[i];
        }
    }

    fn op_fx65(&mut self) {
        let vx = (self.opcode & 0x0f00) >> 8_u16;
        for i in 0..=vx {
            self.registers[i as usize] = self.memory[self.index as usize + i as usize];
        }
    }

    fn cycle(&mut self) {
        self.opcode = ((self.memory[self.pc as usize] as u16) << 8_u16)
            | (self.memory[self.pc as usize + 1] as u16);
        // println!("{:x}", self.opcode);
        self.pc += 2;
        if let Some(&func) = self.op_table.get(&((self.opcode & 0xf000) >> 12)) {
            func(self);
            // println!("execute: {:x}", self.opcode);
            // println!("{:?}", self.registers);
            // println!("{:?}", self.video);
        }
        if self.delay_timer > 0 {
            self.delay_timer -= 1;
        }
        if self.sound_timer > 0 {
            self.sound_timer -= 1;
        }
    }
}

fn main() {
    // init log
    let mut builder = Builder::from_default_env();
    builder.target(Target::Stdout);
    builder.init();
    // init args
    let video_scale = 10;
    let cycle_delay = 1;
    let rom_file_name = "./test_logo.ch8";
    // init emu
    let mut emu = Chip8::new();
    let _ = emu.load_rom(rom_file_name);
    // cycle
    let mut q = 10;
    while q >= 0 {
        // let current_time = Instant::now();
        // if current_time.elapsed().as_millis() > cycle_delay {
        // emu.cycle();
        // }
        emu.cycle();
        q -= 1;
    }
}
